/*******************************************************************************
 * Copyright (c) 2000, 2010 IBM Corporation and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.gef.ui.parts;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;

import org.eclipse.gef.EditPart;
import org.eclipse.gef.EditPartViewer;

/**
 * A utility for synchronizing the selection of multiple EditPartViewers. This
 * class performs selection synchronization by taking the selection from one
 * viewer, and mapping it to the selection in another viewer. The mapping is
 * performed by matching the models of the selected EditParts from one viewer to
 * the EditParts with the same models in another. It can be customized by
 * overriding the {@link #convert(EditPartViewer, EditPart)} method.
 *
 * @author hudsonr
 */
public class SelectionSynchronizer implements ISelectionChangedListener {

	private final List<EditPartViewer> viewers = new ArrayList<>();
	private boolean isDispatching = false;
	private int disabled = 0;
	private EditPartViewer pendingSelection;

	/**
	 * Adds a viewer to the set of synchronized viewers
	 *
	 * @param viewer the viewer
	 */
	public void addViewer(EditPartViewer viewer) {
		viewer.addSelectionChangedListener(this);
		viewers.add(viewer);
	}

	/**
	 * Maps the given editpart from one viewer to an editpart in another viewer. It
	 * returns <code>null</code> if there is no corresponding part. This method can
	 * be overridden to provide custom mapping.
	 *
	 * @param viewer the viewer being mapped to
	 * @param part   a part from another viewer
	 * @return <code>null</code> or a corresponding editpart
	 */
	@SuppressWarnings("static-method")
	protected EditPart convert(EditPartViewer viewer, EditPart part) {
		return viewer.getEditPartForModel(part.getModel());
	}

	/**
	 * Removes the viewer from the set of synchronized viewers
	 *
	 * @param viewer the viewer to remove
	 */
	public void removeViewer(EditPartViewer viewer) {
		viewer.removeSelectionChangedListener(this);
		viewers.remove(viewer);
		if (pendingSelection == viewer) {
			pendingSelection = null;
		}
	}

	/**
	 * Receives notification from one viewer, and maps selection to all other
	 * members.
	 *
	 * @param event the selection event
	 */
	@Override
	public void selectionChanged(SelectionChangedEvent event) {
		if (isDispatching) {
			return;
		}
		EditPartViewer source = (EditPartViewer) event.getSelectionProvider();
		if (disabled > 0) {
			pendingSelection = source;
		} else {
			ISelection selection = event.getSelection();
			syncSelection(source, selection);
		}
	}

	/**
	 * Synchronizes the given selection to all registered viewers except the one
	 * serving as selectionSource, by delegating to
	 * {@link #applySelection(EditPartViewer, ISelection)}.
	 *
	 * @param selectionSource The viewer from which the selection originated.
	 * @param selection       The selection to apply to the other viewers.
	 * @since 3.10
	 */
	protected void syncSelection(EditPartViewer selectionSource, ISelection selection) {
		isDispatching = true;
		for (EditPartViewer viewer : viewers) {
			if (viewer != selectionSource) {
				applySelection(viewer, selection);
			}
		}
		isDispatching = false;
	}

	/**
	 * Enables or disabled synchronization between viewers.
	 *
	 * @since 3.1
	 * @param value <code>true</code> if synchronization should occur
	 */
	public void setEnabled(boolean value) {
		if (!value) {
			disabled++;
		} else if (--disabled == 0 && pendingSelection != null) {
			syncSelection(pendingSelection, pendingSelection.getSelection());
			pendingSelection = null;
		}
	}

	/**
	 * Applies the given EditPart selection from another viewer to the given viewer.
	 * It will first compute a new selection of {@link EditPart}s for the given
	 * viewer by searching those that control the same model elements as the
	 * {@link EditPart}s in the given selection (via
	 * {@link #convert(EditPartViewer, EditPart)}), apply this new selection to the
	 * given viewer, and reveal the last part in the new selection.
	 *
	 * @param viewer    The viewer to apply the given selection to.
	 * @param selection The selection to apply, which has to be an
	 *                  {@link IStructuredSelection} of {@link EditPart}s of another
	 *                  viewer.
	 * @since 3.10
	 */
	protected void applySelection(EditPartViewer viewer, ISelection selection) {
		List<EditPart> result = new ArrayList<>();
		Iterator iter = ((IStructuredSelection) selection).iterator();
		while (iter.hasNext()) {
			EditPart part = convert(viewer, (EditPart) iter.next());
			if (part != null && part.isSelectable()) {
				result.add(part);
			}
		}
		viewer.setSelection(new StructuredSelection(result));
		if (!result.isEmpty()) {
			viewer.reveal(result.get(result.size() - 1));
		}
	}

}
